Skip to content

feat(sync): sync from trusted height#3050

Closed
julienrbrt wants to merge 5 commits intomainfrom
julien/trustedheight
Closed

feat(sync): sync from trusted height#3050
julienrbrt wants to merge 5 commits intomainfrom
julien/trustedheight

Conversation

@julienrbrt
Copy link
Member

@julienrbrt julienrbrt commented Feb 4, 2026

Overview

Re-implement sync from trusted height. With the recent changes, this should be more stable, and a great alternative to snapshot syncing.

@github-actions
Copy link
Contributor

github-actions bot commented Feb 4, 2026

The latest Buf updates on your PR. Results from workflow CI / buf-check (pull_request).

BuildFormatLintBreakingUpdated (UTC)
✅ passed⏩ skipped✅ passed✅ passedFeb 5, 2026, 6:53 AM

@gemini-code-assist
Copy link
Contributor

Summary of Changes

Hello @julienrbrt, I'm Gemini Code Assist1! I'm currently reviewing this pull request and will post my feedback shortly. In the meantime, here's a summary to help you and other reviewers quickly get up to speed!

This pull request enhances the node synchronization mechanism by introducing a "trusted height" feature. This allows nodes to bootstrap their state from a known, verified block height, significantly reducing the time required for initial synchronization, especially in long-running networks. The changes encompass new configuration options, robust validation to ensure secure usage, and integration into the core synchronization and state initialization processes.

Highlights

  • Trusted Height Synchronization: Introduced the ability for nodes to start synchronization from a specified "trusted height" and corresponding header hash, rather than always from genesis.
  • Configuration Parameters: Added TrustedHeight and TrustedHeaderHash to the P2P configuration, allowing users to define a specific starting point for synchronization.
  • Security Validation: Implemented validation to ensure that if a TrustedHeight is provided, a TrustedHeaderHash must also be supplied to prevent syncing from an unverified or malicious state.
  • Core Sync Logic Integration: Modified the Syncer and SyncService components to incorporate the trusted height logic, including fetching and verifying the trusted header from the store and P2P, and initializing the chain state accordingly.
  • Documentation Update: Updated the docs/learn/config.md to provide comprehensive details, examples, and security considerations for the new trusted height synchronization feature.

🧠 New Feature in Public Preview: You can now enable Memory to help Gemini Code Assist learn from your team's feedback. This makes future code reviews more consistent and personalized to your project's style. Click here to enable Memory in your admin console.

Changelog
  • block/internal/syncing/syncer.go
    • Added getTrustedHeader function to load and verify a trusted header from the store.
    • Modified initializeState to conditionally initialize the chain state from a trusted header if configured, otherwise falling back to genesis.
  • docs/learn/config.md
    • Introduced a new 'Sync Configuration' section detailing trusted_height and trusted_header_hash.
    • Provided descriptions, YAML and command-line examples, and security considerations for the new sync options.
  • pkg/config/config.go
    • Defined FlagTrustedHeight and FlagTrustedHeaderHash constants for command-line flags.
    • Added TrustedHeight and TrustedHeaderHash fields to the P2PConfig struct.
    • Implemented validation in Validate() to enforce that TrustedHeaderHash is present when TrustedHeight is set.
    • Registered new command-line flags for TrustedHeight and TrustedHeaderHash.
  • pkg/config/config_test.go
    • Updated the expected flag count in TestAddFlags.
    • Added TestTrustedHeightValidation to test the new configuration validation logic.
  • pkg/config/defaults.go
    • Set default values for TrustedHeight and TrustedHeaderHash in DefaultConfig().
  • pkg/sync/sync_service.go
    • Added trustedHeight and trustedHeaderHash fields to the SyncService struct.
    • Initialized trusted height configuration from P2PConfig during service startup.
    • Modified initFromP2PWithRetry to prioritize fetching and verifying the trusted header from P2P if configured.
    • Adjusted heightToQuery logic to use trustedHeight when the store is empty and trusted height is enabled.
    • Implemented fetchAndVerifyTrustedHeader to fetch and validate a header from P2P at the specified trusted height.
Activity
  • The pull request introduces a new feature for syncing from a trusted height, as indicated by the feat(sync) conventional commit prefix.
  • The author, julienrbrt, has provided a standard PR template, but no additional specific description beyond the title.
  • The changes involve modifications across multiple core components (syncer.go, sync_service.go, config.go) and corresponding documentation and tests.
Using Gemini Code Assist

The full guide for Gemini Code Assist can be found on our documentation page, here are some quick tips.

Invoking Gemini

You can request assistance from Gemini at any point by creating a comment using either /gemini <command> or @gemini-code-assist <command>. Below is a summary of the supported commands on the current page.

Feature Command Description
Code Review /gemini review Performs a code review for the current pull request in its current state.
Pull Request Summary /gemini summary Provides a summary of the current pull request in its current state.
Comment @gemini-code-assist Responds in comments when explicitly tagged, both in pull request comments and review comments.
Help /gemini help Displays a list of available commands.

Customization

To customize Gemini Code Assist for GitHub experience, repository maintainers can create a configuration file and/or provide a custom code review style guide (such as PEP-8 for Python) by creating and adding files to a .gemini/ folder in the base of the repository. Detailed instructions can be found here.

Limitations & Feedback

Gemini Code Assist may make mistakes. Please leave feedback on any instances where its feedback is incorrect or counter productive. You can react with 👍 and 👎 on @gemini-code-assist comments. If you're interested in giving your feedback about your experience with Gemini Code Assist for Github and other Google products, sign up here.

You can also get AI-powered code generation, chat, as well as code reviews directly in the IDE at no cost with the Gemini Code Assist IDE Extension.

Footnotes

  1. Review the Privacy Notices, Generative AI Prohibited Use Policy, Terms of Service, and learn how to configure Gemini Code Assist in GitHub here. Gemini can make mistakes, so double check it and use code with caution.

@claude
Copy link
Contributor

claude bot commented Feb 4, 2026

Claude finished @julienrbrt's task in 1m 54s —— View job


Code Review: Trusted Height Sync Feature

I've completed a comprehensive review of this PR. The trusted height sync feature is well-designed and addresses a real need for faster node bootstrapping. However, I've identified several critical issues that need attention, along with recommendations for security, error handling, and testing improvements.


Critical Issues

1. execReplayer.SyncToHeight Incompatibility with Trusted Height ⚠️ CRITICAL

Location: block/internal/syncing/syncer.go:395-401

Gemini's review identified this correctly. When initializing from a trusted height, the code calls s.exec.InitChain() with the trusted header's height (line 322-330), but then unconditionally calls execReplayer.SyncToHeight(ctx, state.LastBlockHeight) at line 397.

Problem: The replayer is initialized with genesis information and expects to replay blocks sequentially from genesis.InitialHeight. When syncing from trusted height 100000, the store won't have blocks 1-99999, causing GetBlockData to fail in the replayer.

Fix: Skip the replayer sync when initializing from trusted height:

// Sync execution layer with store on startup
// Skip replay if we initialized from trusted height since executor is already at that state
if s.config.P2P.TrustedHeight == 0 || state.LastBlockHeight != s.config.P2P.TrustedHeight {
    execReplayer := common.NewReplayer(s.store, s.exec, s.genesis, s.logger)
    if err := execReplayer.SyncToHeight(s.ctx, state.LastBlockHeight); err != nil {
        return fmt.Errorf("failed to sync execution layer on startup: %w", err)
    }
}

Fix this issue →

2. Documentation Inconsistency ⚠️ HIGH PRIORITY

Location: docs/learn/config.md:1000-1023

The documentation shows the configuration under a sync: section with --evnode.sync.* flags, but the actual implementation places these fields in P2PConfig with --evnode.p2p.* flags.

Current (incorrect) documentation:

sync:
  trusted_height: 100000
  trusted_header_hash: "a1b2c3d4e5f6..."

Should be:

p2p:
  trusted_height: 100000
  trusted_header_hash: "a1b2c3d4e5f6..."
  trusted_data_hash: "d7e8f9g0h1i2..."

Command-line flags should be:

  • --evnode.p2p.trusted_height
  • --evnode.p2p.trusted_header_hash
  • --evnode.p2p.trusted_data_hash

Also note the documentation is missing trusted_data_hash entirely, which is required by the validation logic.


Security Issues

3. Hash Verification Logic Has Potential Security Gaps ⚠️ MEDIUM

Location: pkg/sync/sync_service.go:442-447

The hash verification uses OR logic, accepting if the hash matches EITHER trustedHeaderHash OR trustedDataHash:

if actualHash != syncService.trustedHeaderHash && actualHash != syncService.trustedDataHash {
    return fmt.Errorf("trusted header hash mismatch...")
}

Issues:

  • For header sync service, why would we check against trustedDataHash?
  • For data sync service, why would we check against trustedHeaderHash?
  • The OR logic means if only ONE hash is provided, ANY entity hash would pass if it matches that single hash

Recommendation: The sync service type should determine which hash to validate:

var expectedHash string
if syncService.syncType == headerSync {
    expectedHash = syncService.trustedHeaderHash
} else {
    expectedHash = syncService.trustedDataHash
}

if expectedHash == "" {
    return fmt.Errorf("no trusted hash configured for %s", syncService.syncType)
}

actualHash := trusted.Hash().String()
if actualHash != expectedHash {
    return fmt.Errorf("trusted %s hash mismatch at height %d: expected %s, got %s",
        syncService.syncType, syncService.trustedHeight, expectedHash, actualHash)
}

4. No Hash Format Validation ⚠️ MEDIUM

Location: pkg/config/config.go:386-390

The validation only checks that the hash strings are non-empty, but doesn't validate the format (e.g., hex encoding, expected length).

Recommendation: Add hash format validation to catch user errors early:

if c.P2P.TrustedHeight > 0 {
    if c.P2P.TrustedHeaderHash == "" || c.P2P.TrustedDataHash == "" {
        return fmt.Errorf("trusted_height (%d) requires both trusted_header_hash and trusted_data_hash", c.P2P.TrustedHeight)
    }
    
    // Validate hash format (assuming SHA256 hex encoding = 64 chars)
    if len(c.P2P.TrustedHeaderHash) != 64 {
        return fmt.Errorf("trusted_header_hash must be 64 hex characters (got %d)", len(c.P2P.TrustedHeaderHash))
    }
    if len(c.P2P.TrustedDataHash) != 64 {
        return fmt.Errorf("trusted_data_hash must be 64 hex characters (got %d)", len(c.P2P.TrustedDataHash))
    }
    
    // Validate hex encoding
    if _, err := hex.DecodeString(c.P2P.TrustedHeaderHash); err != nil {
        return fmt.Errorf("trusted_header_hash must be valid hex: %w", err)
    }
    if _, err := hex.DecodeString(c.P2P.TrustedDataHash); err != nil {
        return fmt.Errorf("trusted_data_hash must be valid hex: %w", err)
    }
}

Error Handling & Resilience Issues

5. No Retry Logic for Trusted Header Fetch ⚠️ MEDIUM

Location: pkg/sync/sync_service.go:352-354

The trusted header fetch fails immediately without retry logic, even though the subsequent P2P initialization in initFromP2PWithRetry has exponential backoff (lines 398-428).

Recommendation: Wrap the trusted header fetch in the same retry logic, or incorporate it into the tryInit function's retry loop.

6. Trusted Header Fetch Error Loses Context ⚠️ LOW

Location: pkg/sync/sync_service.go:437-440

When ex.GetByHeight fails, the error message doesn't indicate whether this is a network issue, peer unavailability, or hash mismatch.

Recommendation: Add more context to help operators diagnose issues:

trusted, err := syncService.ex.GetByHeight(ctx, syncService.trustedHeight)
if err != nil {
    return fmt.Errorf("failed to fetch trusted header at height %d from P2P peers (ensure peers are available and synced past this height): %w", 
        syncService.trustedHeight, err)
}

Code Quality & Architecture

7. Unused Parameter Should Be Removed ⚠️ LOW

Location: pkg/sync/sync_service.go:433

Gemini correctly identified that peerIDs []peer.ID is declared but never used in fetchAndVerifyTrustedHeader. The call site at line 352 passes it unnecessarily.

Fix:

func (syncService *SyncService[H]) fetchAndVerifyTrustedHeader(ctx context.Context) error {

And update the call site to remove the argument.

8. Missing Trusted Height in Store Initialization ⚠️ LOW

Location: block/internal/syncing/syncer.go:332-342

When initializing state from trusted height, the code sets state.DAHeight = s.genesis.DAStartHeight. This might be incorrect if the trusted height is significantly ahead of genesis - the DA height should probably be aligned with where the trusted block was posted.

Consideration: Should we also require a TrustedDAHeight configuration parameter? Or query it from the trusted block metadata?

9. State Version Inconsistency ⚠️ LOW

Location: block/internal/syncing/syncer.go:332-342 vs 355-362

When initializing from trusted height, the code sets state.Version = types.InitStateVersion (line 333), but when initializing from genesis it doesn't set any version. This inconsistency could cause issues.

Recommendation: Ensure version is set consistently in both paths.


Testing Coverage Concerns

10. Low Test Coverage (36.48%) ⚠️ HIGH PRIORITY

Codecov reports only 36.48% coverage with 47 lines missing coverage. Critical untested paths include:

block/internal/syncing/syncer.go: 22 missing + 2 partials

  • Trusted header loading and verification (lines 308-342)
  • Error paths in trusted height initialization
  • Edge cases like hash mismatch, missing header

pkg/sync/sync_service.go: 23 missing lines

  • fetchAndVerifyTrustedHeader function (lines 433-460)
  • Hash verification logic
  • Store append failures

Recommendation: Add integration tests covering:

  1. Successful sync from trusted height
  2. Hash mismatch scenarios
  3. Trusted header not available in P2P store
  4. Trusted height with missing/corrupted data
  5. Trusted height = 0 (normal genesis sync)
  6. Config validation for trusted height parameters

11. Existing Test Update Needed ⚠️ MEDIUM

Location: pkg/config/config_test.go:115

The test expects exactly 66 flags, but the PR adds 3 new flags (TrustedHeight, TrustedHeaderHash, TrustedDataHash). The flag count assertion will fail or needs updating to 69.


Documentation & User Experience

12. Missing Security Warnings in Documentation

The documentation should include prominent security warnings:

  • ⚠️ Trust assumption: Users must obtain the trusted hash from a reliable source (e.g., block explorer, trusted node operator)
  • ⚠️ Security trade-off: Syncing from trusted height reduces security guarantees - the node trusts that height without verifying all prior blocks
  • ⚠️ When to use: Suitable for non-critical nodes, testnets, or when combined with other verification methods
  • ⚠️ Not for validators: Validator nodes should sync from genesis for full verification

13. Missing Operational Guidance

Documentation should explain:

  • How to obtain the trusted height and hash (example using block explorer or RPC)
  • What happens if the hash is wrong (node will fail to start with clear error)
  • How to fall back to genesis sync if needed (remove the flags)
  • Performance expectations (e.g., "Syncing from height 100k instead of genesis reduces sync time from X hours to Y minutes")

Positive Aspects ✅

  1. Clean separation of concerns - Config validation, P2P fetching, and state initialization are well-separated
  2. Good logging - Informative log messages at key points (lines 310, 434, 449-451)
  3. Validation at config level - Catches misconfiguration before node startup
  4. Works with existing architecture - Integrates cleanly with P2P sync and store initialization
  5. Test coverage for config validation - TestTrustedHeightValidation covers basic validation scenarios

Summary & Recommendations

This PR implements a valuable feature that will significantly improve user experience for node operators. However, the critical issue with execReplayer.SyncToHeight must be fixed before merging, as it will cause nodes to fail when using trusted height sync.

Before Merging:

  1. ✅ Fix the replayer sync issue (Critical)
  2. ✅ Correct the documentation (High priority)
  3. ✅ Improve hash verification logic (Security)
  4. ✅ Add hash format validation (Security)
  5. ✅ Add integration tests for trusted height scenarios (High priority)
  6. ✅ Remove unused peerIDs parameter (Code quality)

Nice to Have:

  • Add retry logic for trusted header fetch
  • Improve error messages with more context
  • Add security warnings to documentation
  • Consider requiring TrustedDAHeight parameter
  • Add operational guidance for users

Overall Assessment: The architecture and approach are sound, but critical bugs and security improvements are needed before this can be safely deployed.

Copy link
Contributor

@gemini-code-assist gemini-code-assist bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request introduces a valuable feature for faster node synchronization by starting from a trusted height. The implementation is spread across configuration, synchronization, and storage logic, and is mostly well-executed. I've identified a potential critical issue in the state initialization logic that could prevent nodes from starting correctly with a trusted height, along with some inconsistencies in the documentation and a minor code cleanup opportunity. Once these points are addressed, this will be a solid contribution.

Comment on lines 336 to 365
if s.config.P2P.TrustedHeight > 0 {
s.logger.Info().Uint64("trusted_height", s.config.P2P.TrustedHeight).Msg("initializing state from trusted height")

// Load and verify the trusted header
trustedHeader, err := s.getTrustedHeader(s.ctx)
if err != nil {
return fmt.Errorf("failed to load trusted header: %w", err)
}

state = types.State{
ChainID: s.genesis.ChainID,
InitialHeight: s.genesis.InitialHeight,
LastBlockHeight: s.genesis.InitialHeight - 1,
LastBlockTime: s.genesis.StartTime,
DAHeight: s.genesis.DAStartHeight,
AppHash: stateRoot,
// Initialize new chain state from the trusted header
stateRoot, initErr := s.exec.InitChain(
s.ctx,
trustedHeader.Time(),
trustedHeader.Height(),
trustedHeader.ChainID(),
)
if initErr != nil {
return fmt.Errorf("failed to initialize execution client: %w", initErr)
}

state = types.State{
Version: types.InitStateVersion,
ChainID: trustedHeader.ChainID(),
InitialHeight: trustedHeader.Height(),
LastBlockHeight: trustedHeader.Height(),
LastBlockTime: trustedHeader.Time(),
LastHeaderHash: trustedHeader.Hash(), // Hash of the trusted header
DAHeight: s.genesis.DAStartHeight,
AppHash: stateRoot,
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

critical

This new logic for initializing from a trusted height looks mostly correct, but there's a potential critical issue with a subsequent call outside this block. After this block, execReplayer.SyncToHeight is called at line 421. When initializing from a trusted height, this can lead to a failure.

The replayer is initialized with the original genesis information and will likely try to replay blocks from genesis.InitialHeight. However, for a trusted sync, the store will not contain these intermediate blocks, causing GetBlockData to fail.

Since s.exec.InitChain is already called to set the executor's state to the trusted height, the subsequent replayer call seems unnecessary and problematic for this case. Consider restructuring the logic to skip the execReplayer.SyncToHeight call when the state is initialized from a trusted height.

Comment on lines +1002 to +1019
```yaml
sync:
trusted_height: 100000 # Block height to trust for sync initialization
trusted_header_hash: "a1b2c3d4e5f6..." # Hex-encoded hash of the header at trusted_height
```

---
**Command-line Flags:**

- `--evnode.sync.trusted_height <uint64>` - Block height to trust for sync initialization
- `--evnode.sync.trusted_header_hash <string>` - Hash of the trusted header for security verification (hex-encoded)

**Example:**

```bash
testapp start \
--evnode.sync.trusted_height 100000 \
--evnode.sync.trusted_header_hash "abc123def456..."
```
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The documentation for the new trusted sync feature appears to be inconsistent with the implementation in pkg/config/config.go.

  1. YAML Configuration: The documentation places trusted_height and trusted_header_hash under a sync: section. However, the code defines these fields within P2PConfig, which is mapped to a p2p: section in YAML.
  2. Command-line Flags: The documentation lists the flags as --evnode.sync.trusted_height and --evnode.sync.trusted_header_hash. The implementation defines them as --evnode.p2p.trusted_height and --evnode.p2p.trusted_header_hash.

To avoid user confusion, the documentation should be updated to reflect the implementation. Here is a suggested correction:

**YAML:**

```yaml
p2p:
  trusted_height: 100000 # Block height to trust for sync initialization
  trusted_header_hash: "a1b2c3d4e5f6..." # Hex-encoded hash of the header at trusted_height

Command-line Flags:

  • --evnode.p2p.trusted_height <uint64> - Block height to trust for sync initialization
  • --evnode.p2p.trusted_header_hash <string> - Hash of the trusted header for security verification (hex-encoded)

Example:

testapp start \
  --evnode.p2p.trusted_height 100000 \
  --evnode.p2p.trusted_header_hash "abc123def456..."


// fetchAndVerifyTrustedHeader fetches the header at the trusted height from P2P
// and verifies it matches the trusted hash. If verification passes, it stores the header.
func (syncService *SyncService[H]) fetchAndVerifyTrustedHeader(ctx context.Context, peerIDs []peer.ID) error {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The peerIDs parameter is unused within this function and should be removed to simplify the signature. The call site at line 351 should be updated accordingly by removing the peerIDs argument.

Suggested change
func (syncService *SyncService[H]) fetchAndVerifyTrustedHeader(ctx context.Context, peerIDs []peer.ID) error {
func (syncService *SyncService[H]) fetchAndVerifyTrustedHeader(ctx context.Context) error {

@codecov
Copy link

codecov bot commented Feb 4, 2026

Codecov Report

❌ Patch coverage is 36.00000% with 48 lines in your changes missing coverage. Please review.
✅ Project coverage is 56.18%. Comparing base (d20b1ac) to head (8a0e65d).
⚠️ Report is 1 commits behind head on main.

Files with missing lines Patch % Lines
block/internal/syncing/syncer.go 35.89% 23 Missing and 2 partials ⚠️
pkg/sync/sync_service.go 11.53% 23 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main    #3050      +/-   ##
==========================================
- Coverage   56.38%   56.18%   -0.20%     
==========================================
  Files         118      118              
  Lines       12036    12093      +57     
==========================================
+ Hits         6787     6795       +8     
- Misses       4507     4555      +48     
- Partials      742      743       +1     
Flag Coverage Δ
combined 56.18% <36.00%> (-0.20%) ⬇️

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@julienrbrt
Copy link
Member Author

Closing, as we'll still need snapshots for ev-reth, which does not improve the UX.

@julienrbrt julienrbrt closed this Feb 5, 2026
@julienrbrt julienrbrt deleted the julien/trustedheight branch February 5, 2026 09:28
@github-actions
Copy link
Contributor

github-actions bot commented Feb 5, 2026

PR Preview Action v1.8.1
Preview removed because the pull request was closed.
2026-02-05 09:29 UTC

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant